/**
 * projections
 *
 * Factory methods for constructing decorated d3 projections that are serializable and copyable. Also adds an
 * optimize() method which returns a faster implementation of the projection for temporary intensive use.
 */

// import * as d3 from "../lib/d3.js";
import * as d3 from "d3";
import * as proD3 from "d3-geo-projection";
import {orthographicProjection, orthographicArgsFromD3} from "./orthographic.js";
import {equirectangularProjection, equirectangularArgsFromD3} from "./equirectangular.js";

const factories = {
    conicEquidistant: newConicEquidistant,
    equirectangular: newEquirectangular,
    mollweide: newMollweide,
    orthographic: newOrthographic,
    patterson: newPatterson,
    polyhedralWaterman: newPolyhedralWaterman,
    stereographic: newStereographic,
    winkel3: newWinkel3,
};

/**
 * @returns {{project: function, invert: function}} the default optimized interface for the projection.
 */
function optimize() {
    // The default optimization for a D3 projection reuses the coord/point array to avoid creating garbage.
    const project_d3 = copy(this);
    const invert_d3 = project_d3.invert.bind(project_d3);
    const buffer = [NaN, NaN];
    return {
        project(lon, lat) {
            buffer[0] = lon;
            buffer[1] = lat;
            return project_d3(buffer) ?? [NaN, NaN];
        },
        invert(x, y) {
            buffer[0] = x;
            buffer[1] = y;
            return invert_d3(buffer) ?? [NaN, NaN];
        },
    };
}

export function newConicEquidistant() {
    return Object.assign(d3.geoConicEquidistant(), {key: "conicEquidistant", optimize});
}

export function newEquirectangular() {
    return Object.assign(d3.geoEquirectangular(), {
        key: "equirectangular",
        optimize() {
            return equirectangularProjection(...equirectangularArgsFromD3(this));
        },
    });
}

export function newMollweide() {
    return Object.assign(proD3.geoMollweide(), {key: "mollweide", optimize});
}

export function newOrthographic() {
    return Object.assign(d3.geoOrthographic(), {
        key: "orthographic",
        optimize() {
            return orthographicProjection(...orthographicArgsFromD3(this));
        }
    });
}

export function newPatterson() {
    return Object.assign(proD3.geoPatterson(), {key: "patterson", optimize});
}

export function newPolyhedralWaterman() {
    return Object.assign(proD3.geoPolyhedralWaterman(), {key: "polyhedralWaterman", optimize});
}

export function newStereographic() {
    return Object.assign(d3.geoStereographic(), {key: "stereographic", optimize});
}

export function newWinkel3() {
    return Object.assign(proD3.geoWinkel3(), {key: "winkel3", optimize});
}

/**
 * @param proj a decorated d3 projection.
 * @returns {string} the serialized form of the projection.
 */
export function stringify(proj) {
    if (factories[proj.key] === undefined) {
        throw new Error("Unsupported projection: " + proj);
    }
    return JSON.stringify([
        proj.key,           // 0
        proj.clipAngle(),   // 1
        proj.clipExtent(),  // 2
        proj.scale(),       // 3
        proj.translate(),   // 4
        proj.center(),      // 5
        proj.angle(),       // 6
        proj.rotate(),      // 7
        proj.precision(),   // 8
    ]);
}

/**
 * @param {string} json a serialized d3 projection as generated by {@link stringify}.
 * @returns {Function} the deserialized decorated d3 projection.
 */
export function parse(json) {
    const payload = JSON.parse(json);
    const key = payload[0];
    const factory = factories[key];
    if (factory === undefined) {
        throw new Error("Unknown projection: " + key);
    }
    return factory()
        .clipAngle(payload[1])
        .clipExtent(payload[2])
        .scale(payload[3])
        .translate(payload[4])
        .center(payload[5])
        .angle(payload[6])
        .rotate(payload[7])
        .precision(payload[8]);
}

/**
 * @param proj a decorated d3 projection.
 * @returns {Function} a copy of the projection.
 */
export function copy(proj) {
    return parse(stringify(proj));
}
